#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */ 
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
using Adoor.Object.Query;

namespace Adoor.Object.Domain
{
	/// <summary>
	/// The starting point for entity metadata.
	/// </summary>
	public class EntityData
	{
		public EntityData(ObjectDomain domain, Type entityType, XmlNode metadata, XmlNamespaceManager nsmgr, System.Reflection.Assembly implementationAssembly, bool generate)
		{
			this.propertyInfos = new PropertyDataCollection(this);
			this.referenceInfos = new ReferenceDataCollection(this);
			this.domain = domain;
			this.entityType = entityType;
			this.isAbstract = metadata.SelectSingleNode("typing:entity[@abstract='true']", nsmgr) != null;
			XmlNode classIdNode = metadata.SelectSingleNode("typing:entity/@classId", nsmgr);
			this.classId = classIdNode == null ? "" : classIdNode.Value;
			Type parentType;
			if(!generate)
				parentType = getParentType(metadata, nsmgr);
			else
				parentType = getParentType(metadata, nsmgr, implementationAssembly);
			
			if (parentType != null)
			{				
				this.parentData = domain.EntityInfos[parentType];
				if (this.parentData == null)
					throw new Exception("factory not found for type " + parentType.Name);
				this.parentData.RegisterChild(this);
			}

			XmlNode keyManagerNode = metadata.SelectSingleNode("typing:entity/@keyManager", nsmgr);
			if (keyManagerNode == null)
				this.polymorphismManager = null;
			else
			{
				Type keyManagerType = Type.GetType(keyManagerNode.Value, true);
				this.polymorphismManager = (IPolymorphismManager)keyManagerType.GetConstructor(new Type[] {}).Invoke(null);
			}
		}

		public EntityData()
		{
			this.propertyInfos = new PropertyDataCollection(this);
			this.referenceInfos = new ReferenceDataCollection(this);
		}

		private string classId;
		public string ClassId
		{
			get { return this.classId; }
		}

		private bool isAbstract;
		public bool IsAbstract
		{
			get { return this.isAbstract; }
		}

		private IPolymorphismManager polymorphismManager;
		/// <summary>
		/// It is only when polymorphism is really needed that we add the machinery to handle children and polymorphism.
		/// A polymorphic branch is defined as the set consisting of a polymorphic entity, its children and (recursively)
		/// the children of its children.
		/// Inside a polymorphic branch, OIDs must be unique. Between two unrelated polymorphic branches,
		/// OIDs may not be unique. The polymorphism manager retreives the entity factory associated
		/// with a particular OID inside a polymorphic branch.
		/// </summary>
		public IPolymorphismManager PolymorphismManager
		{
			get { return this.polymorphismManager; }
		}

		private static Type getParentType(XmlNode metadata, XmlNamespaceManager nsmgr)
		{
			XmlNode node = metadata.SelectSingleNode("typing:entity/@parent", nsmgr);
			if (node == null)
				return null;
			string typeName = node.Value;
			Type type = Type.GetType(typeName, true);
			return type;
		}

		private static Type getParentType(XmlNode metadata, XmlNamespaceManager nsmgr, System.Reflection.Assembly assembly)
		{
			XmlNode node = metadata.SelectSingleNode("typing:entity/@parent", nsmgr);
			if (node == null)
				return null;
			string typeName = node.Value.Split(',')[0];
			Type type = assembly.GetType(typeName, true);
			return type;
		}

		private EntityData parentData;

		public EntityData ParentData
		{
			get { return this.parentData; }
		}

		/// <summary>
		/// oid is the id of a (concrete) entity which we know inherits from this type.
		/// return the entity data corresponding to the oid.
		/// </summary>
		/// <param name="oid"></param>
		/// <returns></returns>
		public EntityData GetActualEntityData(string oid)
		{
			return GetActualEntityData(oid, true);
		}

		public EntityData GetActualEntityData(string oid, bool throwIfNotFound)
		{
			if (this.PolymorphismManager == null)
				return this;
			return this.PolymorphismManager.GetActualEntityData(oid, throwIfNotFound);
		}

		public ObjectDomain Domain
		{ 
			get { return this.domain; }			
		}

		public virtual IOidData OidData
		{
			get { return this.oidData; }
			set 
			{
				if (this.oidData != null)
					throw new Exception("cannot change OidFactory");
				this.oidData = value;
			}
		}

		private PropertyDataCollection propertyInfos;
		public PropertyDataCollection PropertyInfos
		{
			get { return this.propertyInfos; }
		}

		private ReferenceDataCollection referenceInfos;
		public ReferenceDataCollection ReferenceInfos
		{
			get { return this.referenceInfos; }
		}

		public virtual void RegisterPropertyData(string name, PropertyData propertyData)
		{
            if(!this.propertyInfos.ContainsKey(name))
			    this.propertyInfos.Add(name, propertyData);
		}

		public virtual void RegisterReferenceData(string role, ReferenceData referenceData)
		{
            if (!this.propertyInfos.ContainsKey(role))
                this.referenceInfos.Add(role, referenceData);
		}

		public virtual Type EntityType
		{
			get { return this.entityType; }
		}

		public ObjectPath GetPath(string alias)
		{
			return new ScanPath(this, alias, Domain.ObjectResolver);
		}

        private Dictionary<Type, object> services = new Dictionary<Type, object>();

		public object GetService(Type type)
		{
            object result;
            if (!this.services.TryGetValue(type, out result))
                return null;
            return result;
		}

		public void RegisterService(Type type, object service)
		{
			this.services[type] = service;
		}

		public void RegisterChild(EntityData entityData)
		{
			if (this.PolymorphismManager == null)
				throw new Exception("attribute @KeyManager not found for type " + this.EntityType.Name);
			this.children.Add(entityData);
		}

		public EntityData FindChild(string name)
		{
			foreach(EntityData child in this.children)
			{
				if(child.EntityType.Name.ToLower() == name.ToLower())
					return child;
				
				EntityData subChild = child.FindChild(name);
				if(subChild != null)
					return subChild;
			}
			return null;
		}

        protected List<Adoor.Object.Domain.EntityData> children = new List<EntityData>();

        public List<Adoor.Object.Domain.EntityData> ChildrenInfo
		{
			get { return this.children; }
		}

		private ObjectDomain domain;
		private Type entityType;
		private IOidData oidData;
        private System.Collections.IDictionary extendedProperties = null;

        public System.Collections.IDictionary ExtendedProperties
        {
            get
            {
                if (extendedProperties == null)
                    extendedProperties = new System.Collections.Hashtable();
                return extendedProperties;
            }
        }

		public event UpdateEventHandler Update;
		
		public void RaiseUpdateEvent(object transaction)
		{
			if(Update !=null)
				Update(this, new UpdateEventArgs(transaction));
		}
	}

	public delegate void UpdateEventHandler(object sender, UpdateEventArgs e);

	public class UpdateEventArgs : EventArgs
	{
		public UpdateEventArgs(object transaction)
		{
			this.transaction = transaction;
		}

		private object transaction;
		public object Transaction
		{
			get{ return transaction; }
		}
	}
}